﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using OpenSearch.Client;
using OpenSearch.Client.Util;
using Wpf.Controls;

namespace OpenSearch.Controls
{
    [TemplatePart(Name = Part_AutoCompleteBox, Type = typeof (AutoCompleteBox))]
    [TemplatePart(Name = Part_Button, Type = typeof (SplitButton))]
    public class SearchBox : Control
    {
        public const string Part_AutoCompleteBox = "PART_AutoCompleteBox";
        public const string Part_Button = "PART_SearchButton";

        public static readonly RoutedEvent SearchRequestedEvent =
            EventManager.RegisterRoutedEvent("SearchRequested", RoutingStrategy.Bubble,
                                             typeof (SearchRequestedEventHandler), typeof (SearchBox));

        /// <summary>
        ///   CurrentPageUri Dependency Property
        /// </summary>
        public static readonly DependencyProperty CurrentPageUriProperty =
            DependencyProperty.Register("CurrentPageUri", typeof (Uri), typeof (SearchBox),
                                        new FrameworkPropertyMetadata(null,
                                                                      new PropertyChangedCallback(
                                                                          OnCurrentPageUriChanged)));

        /// <summary>
        ///   SearchTerms Dependency Property
        /// </summary>
        public static readonly DependencyProperty SearchTermsProperty =
            DependencyProperty.Register("SearchTerms", typeof (string), typeof (SearchBox),
                                        new FrameworkPropertyMetadata(null,
                                                                      FrameworkPropertyMetadataOptions.
                                                                          BindsTwoWayByDefault));

        /// <summary>
        ///   RequestedSearchUri Dependency Property
        /// </summary>
        public static readonly DependencyProperty RequestedSearchUriProperty =
            DependencyProperty.Register("RequestedSearchUri", typeof (Uri), typeof (SearchBox),
                                        new FrameworkPropertyMetadata((Uri) null));

        /// <summary>
        ///   SearchManager Dependency Property
        /// </summary>
        public static readonly DependencyProperty SearchManagerProperty =
            DependencyProperty.Register("SearchManager", typeof (SearchClientManager), typeof (SearchBox),
                                        new FrameworkPropertyMetadata(null,
                                                                      new PropertyChangedCallback(OnSearchManagerChanged)));

        /// <summary>
        ///   CurrentClient Dependency Property
        /// </summary>
        public static readonly DependencyProperty CurrentClientProperty =
            DependencyProperty.Register("CurrentClient", typeof (SearchClient), typeof (SearchBox),
                                        new FrameworkPropertyMetadata(null,
                                                                      new PropertyChangedCallback(OnCurrentClientChanged)));

        private AutoCompleteBox _autoCompleteBox;
        private SplitButton _searchButton;
        private bool _inSuggest;

        private CancellationTokenSource _source;

        static SearchBox()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof (SearchBox),
                                                     new FrameworkPropertyMetadata(typeof (SearchBox)));

            EventManager.RegisterClassHandler(typeof (SearchBox), Keyboard.PreviewKeyDownEvent,
                                              new KeyEventHandler(OnPreviewKeyDown));
        }

        /// <summary>
        ///   Gets or sets the CurrentPageUri property. This dependency property 
        ///   indicates ....
        /// </summary>
        public Uri CurrentPageUri
        {
            get { return (Uri) GetValue(CurrentPageUriProperty); }
            set { SetValue(CurrentPageUriProperty, value); }
        }

        /// <summary>
        ///   Gets or sets the SearchTerms property. This dependency property 
        ///   indicates ....
        /// </summary>
        public string SearchTerms
        {
            get { return (string) GetValue(SearchTermsProperty); }
            set { SetValue(SearchTermsProperty, value); }
        }

        /// <summary>
        ///   Gets or sets the RequestedSearchUri property. This dependency property 
        ///   indicates ....
        /// </summary>
        public Uri RequestedSearchUri
        {
            get { return (Uri) GetValue(RequestedSearchUriProperty); }
            set { SetValue(RequestedSearchUriProperty, value); }
        }

        /// <summary>
        ///   Gets or sets the SearchManager property. This dependency property 
        ///   indicates ....
        /// </summary>
        public SearchClientManager SearchManager
        {
            get { return (SearchClientManager) GetValue(SearchManagerProperty); }
            set { SetValue(SearchManagerProperty, value); }
        }

        /// <summary>
        ///   Gets or sets the CurrentClient property. This dependency property 
        ///   indicates ....
        /// </summary>
        public SearchClient CurrentClient
        {
            get { return (SearchClient) GetValue(CurrentClientProperty); }
            set { SetValue(CurrentClientProperty, value); }
        }

        public event SearchRequestedEventHandler SearchRequested
        {
            add { AddHandler(SearchRequestedEvent, value); }
            remove { RemoveHandler(SearchRequestedEvent, value); }
        }


        private static void OnPreviewKeyDown(object sender, KeyEventArgs e)
        {
            var searchBox = (SearchBox) sender;

            searchBox.HandlePreviewKeyDown(e);
        }

        private void HandlePreviewKeyDown(KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                OnSearchRequested(false);
            }
            else if (e.Key == Key.System && e.SystemKey == Key.Enter)
            {
                // Handle Alt-Enter
                OnSearchRequested(true);
                _autoCompleteBox.IsDropDownOpen = false;
                e.Handled = true;
            }
        }

        /// <summary>
        ///   Handles changes to the CurrentPageUri property.
        /// </summary>
        private static void OnCurrentPageUriChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var target = (SearchBox) d;
            var oldCurrentPageUri = (Uri) e.OldValue;
            Uri newCurrentPageUri = target.CurrentPageUri;
            target.OnCurrentPageUriChanged(oldCurrentPageUri, newCurrentPageUri);
        }

        /// <summary>
        ///   Provides derived classes an opportunity to handle changes to the CurrentPageUri property.
        /// </summary>
        protected virtual void OnCurrentPageUriChanged(Uri oldCurrentPageUri, Uri newCurrentPageUri)
        {
            if (newCurrentPageUri != null)
            {
                if (SearchManager != null)
                    SearchManager.SetCurrentUri(newCurrentPageUri);
            }
        }

        /// <summary>
        ///   Handles changes to the SearchManager property.
        /// </summary>
        private static void OnSearchManagerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var target = (SearchBox) d;
            var oldSearchManager = (SearchClientManager) e.OldValue;
            SearchClientManager newSearchManager = target.SearchManager;
            target.OnSearchManagerChanged(oldSearchManager, newSearchManager);
        }

        /// <summary>
        ///   Provides derived classes an opportunity to handle changes to the SearchManager property.
        /// </summary>
        protected virtual void OnSearchManagerChanged(SearchClientManager oldSearchManager,
                                                      SearchClientManager newSearchManager)
        {
            Unwire();

            if (newSearchManager != null)
            {
                Wire();
                CurrentClient = newSearchManager.Clients.FirstOrDefault();
            }
            else
            {
                CurrentClient = null;
            }
        }

        /// <summary>
        ///   Handles changes to the CurrentClient property.
        /// </summary>
        private static void OnCurrentClientChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var target = (SearchBox) d;
            var oldCurrentClient = (SearchClient) e.OldValue;
            SearchClient newCurrentClient = target.CurrentClient;
            target.OnCurrentClientChanged(oldCurrentClient, newCurrentClient);
        }

        /// <summary>
        ///   Provides derived classes an opportunity to handle changes to the CurrentClient property.
        /// </summary>
        protected virtual void OnCurrentClientChanged(SearchClient oldCurrentClient, SearchClient newCurrentClient)
        {
            if (_source != null)
            {
                _source.Cancel();
                _source = null;
            }

            if (newCurrentClient != null)
            {
                Unwire();
                Wire();

                // Refresh
                _source = new CancellationTokenSource();

                RefreshSuggestions(newCurrentClient, _source.Token);
            }
        }


        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            _autoCompleteBox = (AutoCompleteBox) GetTemplateChild(Part_AutoCompleteBox);

            _autoCompleteBox.Populating += AutoCompleteBox_Populating;
            _autoCompleteBox.SelectionChanged += AutoCompleteBox_SelectionChanged;

            _searchButton = (SplitButton) GetTemplateChild(Part_Button);
            _searchButton.Click += SearchButton_Click;


            var binding = new Binding("CurrentClient.SearchTerms")
                              {
                                  Mode = BindingMode.TwoWay,
                                  Source = this
                              };

            BindingOperations.SetBinding(_autoCompleteBox, AutoCompleteBox.TextProperty, binding);


            var termsBinding = new Binding("CurrentClient.SearchTerms")
                                   {
                                       Mode = BindingMode.TwoWay,
                                       Source = this
                                   };

            SetBinding(SearchTermsProperty, termsBinding);

            Wire();
        }

        private void SearchButton_Click(object sender, RoutedEventArgs e)
        {
            OnSearchRequested(false);
        }

        private void AutoCompleteBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (CurrentClient == null)
                return;

            var suggestions = e.AddedItems.Cast<SearchSuggestion>();
            if (suggestions.Any())
            {
                var text = suggestions.First().Text;
                // Push into all clients
                CurrentClient.SearchTerms = text;
            }
        }

        private void AutoCompleteBox_Populating(object sender, PopulatingEventArgs e)
        {
            if (_source != null)
            {
                _source.Cancel();
                _source = null;
            }


            if (CurrentClient == null)
                return;

            _inSuggest = true;

            CurrentClient.SearchTerms = e.Parameter;

            e.Cancel = true;

            // Create a new token
            _source = new CancellationTokenSource();

            RefreshSuggestions(CurrentClient, _source.Token);
        }

        private async void RefreshSuggestions(SearchClient client, CancellationToken token)
        {
            await client.RefreshSuggestions(token);

            if (_inSuggest)
            {
                _autoCompleteBox.PopulateComplete();
                _inSuggest = false;
            }
        }

        private void OnSearchRequested(bool newWindow)
        {
            if (_source != null)
            {
                _source.Cancel();
                _source = null;
            }

            _inSuggest = false;

            if (CurrentClient != null)
                RequestedSearchUri = CurrentClient.SearchUri;

            RaiseEvent(new SearchRequestedEventArgs(RequestedSearchUri, newWindow));
        }


        private void Unwire()
        {
            // unwire old one
            if (_searchButton != null)
            {
                foreach (MenuItem menuitem in _searchButton.Items)
                {
                    var handler = (RoutedEventHandler) menuitem.Tag;
                    menuitem.Click -= handler;
                }

                _searchButton.Items.Clear();
            }
        }

        private void Wire()
        {
            if (_searchButton != null && SearchManager != null)
                foreach (var client in SearchManager.Clients)
                {
                    var localClient = client;
                    RoutedEventHandler onClick = (s, e) => OnMenuItemClick(localClient);

                    var menuItem = new MenuItem
                                       {
                                           Header = new TextBlock
                                                        {
                                                            Text = client.Name,
                                                            FontWeight =
                                                                localClient == CurrentClient
                                                                    ? FontWeights.Bold
                                                                    : FontWeights.Normal
                                                        },
                                           Icon = new Image
                                                      {
                                                          Source = new BitmapImage(localClient.ImageLocation)
                                                      },
                                           Tag = onClick
                                       };

                    menuItem.Click += onClick;

                    _searchButton.Items.Add(menuItem);
                }
        }

        private void OnMenuItemClick(SearchClient client)
        {
            CurrentClient = client;

            OnSearchRequested(false);
        }
    }
}